Skip to content

[pull] main from lobehub:main#495

Open
pull[bot] wants to merge 4162 commits into
code:mainfrom
lobehub:main
Open

[pull] main from lobehub:main#495
pull[bot] wants to merge 4162 commits into
code:mainfrom
lobehub:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Jan 27, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

@pull pull Bot locked and limited conversation to collaborators Jan 27, 2026
@pull pull Bot added ⤵️ pull merge-conflict Resolve conflicts manually labels Jan 27, 2026
arvinxx and others added 27 commits May 17, 2026 17:06
…man topic status (#14870)

* ✨ feat(cc): support TaskCreate / TaskUpdate / TaskList tools (CC 2.1.143+)

Add adapter accumulator, inspectors and Todos panel for CC's imperative
task trio that replaces TodoWrite. TaskUpdate's status flip is surfaced
as a per-call chip ("Completed: Read hosts") and the Todos panel header
mirrors that label, with subject resolved from pluginState by CC-assigned
task id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(cc): escape-toggle AskUserQuestion + waitingForHuman topic status

AskUserQuestion intervention — mode-exclusive escape hatch:
- Mirror `lobe-user-interaction`'s "Or type directly" toggle: form picks
  and the freeform reply are mutually exclusive, not stacked. Default
  view shows the multi-choice options; clicking "Or type directly"
  swaps the body to a single TextArea, and "Back to options" returns.
- Submit sends either per-question picks OR `{ __freeform__: <text> }`
  (never both). Bridge formatter (`AskUserMcpServer.formatAnswerForCC`)
  forwards the text verbatim to CC when `__freeform__` is the payload,
  bypassing the `User answers:\n- <q>: <a>` framing — keeps the model
  prompt clean when the user opts out of the structured form.
- Draft persistence resumes the user back into escape mode when
  `__freeform__` is non-empty; an empty draft starts in form mode.
  Timeout fallback respects escape mode: non-empty text submits as-is
  rather than being discarded for option-1-of-each defaults.
- Render swaps to a single "user reply" card with the typed text when
  `__freeform__` is present; otherwise renders the Q&A pairs as before.

Topic status `waitingForHuman`:
- Add new enum value to `ChatTopic` status — TS-only widening (the
  drizzle `text({enum})` is not a `pgEnum`, no migration needed) —
  wired through types + zod router schema.
- Sidebar topic row renders a warning-colored Hand icon when an
  intervention is pending so the waiting state reads from the topic list.
- `heterogeneousAgentExecutor` flips status to `waitingForHuman` when
  an AskUser intervention is raised and back to `running` once the
  bridge resolves; `conversationControl.submitHeteroIntervention` also
  flips back to `running` after the user submits / skips / cancels. The
  natural `runtime_end → writeTopicStatus('active')` takes over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(explorer-tree): drop doubled outline on selected file rows

Add `--trees-selected-focused-border-color-override: transparent` to
both ExplorerTree consumers (working-sidebar Files + AgentDocuments).
`@pierre/trees` draws an outline via `::before` on focused+selected
rows that visually fights with the filled `--trees-selected-bg`
highlight — the existing `--trees-border-color-override: transparent`
only controls structural borders, not this focus outline. Keyboard
focus ring on unselected rows stays intact (a11y).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4880)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…kill (#14884)

* ✨ feat(linear): share branded inspector between CC MCP and built-in Linear skill

The Linear-branded inspector (logomark + action chip + parentId badge) was
only registered against `mcp__claude_ai_Linear__*` tool names emitted by the
CC adapter. LobeHub's own built-in Linear skill calls land with
`identifier='linear'` and bare apiNames (`get_issue`, `save_issue`, …), so
they fell through to the generic Title + JSON inspector despite being the
exact same Linear surface.

Moves the inspector + label utilities out of `builtin-tool-claude-code` into
`packages/builtin-tools/src/linear/` (alongside `github/`) and registers
them twice in the central inspector map: once under `LinearIdentifier =
'linear'` for the built-in skill path, once merged into the CC entry for
the MCP-prefixed wire names. Same component, same look in both cases.

`formatLinearShortLabel` now matches bare apiNames against the known tool
list too, so the collapsed workflow summary reads `Linear · Get issue`
for built-in calls as well — previously only CC got the humanized label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(linear): leave CC's LinearMcp inspector inside CC, only ship the built-in skill side

Walks back the cross-package edits from the previous commit. The CC adapter
keeps its own `LinearMcp.tsx` + `linearMcpLabels.ts` exactly as #14864 left
them — `formatLinearMcpShortLabel` is still exported from
`@lobechat/builtin-tool-claude-code/client/labels` and `toolDisplayNames.ts`
still imports it from there. CC's inspector index continues to spread
`LinearMcpInspectors` into its own map.

The new shared module under `packages/builtin-tools/src/linear/` now only
covers the built-in LobeHub Linear skill path: `LinearIdentifier='linear'`
+ bare apiNames (`get_issue`, `save_issue`, …). The inspector component is
duplicated from CC on purpose — `builtin-tools` already depends on
`builtin-tool-claude-code`, so we can't import the other way without a
circular dep, and the user wants the CC code to stay put.

Drops the `LinearMcpInspectors` re-export and the CC-entry merge in
`inspectors.ts` that the previous commit had introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(linear): hoist shared LinearInspector + label utilities into shared-tool-ui

The Linear-branded inspector and its tool-name parsing helpers were
duplicated between `builtin-tool-claude-code/src/client/Inspector/LinearMcp`
(MCP-prefixed wire names) and `builtin-tools/src/linear/` (built-in skill
bare names). The dep graph (`builtin-tools` → `builtin-tool-claude-code` →
`shared-tool-ui`) means CC can't import from `builtin-tools`, so the
previous round kept two copies.

Moves the component + labels into `packages/shared-tool-ui/src/Inspector/
Linear/` — both CC and `builtin-tools` already depend on `shared-tool-ui`,
so they can each pull the same `LinearInspector` and register it under
whichever key shape their code path uses:

- CC's `LinearMcp.tsx` is now a 10-line wrapper that maps the shared
  inspector across every MCP-prefixed name.
- CC's `linearMcpLabels.ts` re-exports the parsing primitives + keeps the
  CC-only `formatLinearMcpShortLabel` (the prefix check stays here so the
  workflow-summary label only fires for MCP-prefixed wire names).
- `builtin-tools/src/linear/` drops its own Inspector / labels files; the
  index just registers the shared component under bare apiNames.

Exposes a labels-only subpath `@lobechat/shared-tool-ui/inspectors/
linear-labels` so the workflow-summary path can pull parsing helpers
without dragging the React inspector (and its `keyframes`-using style
modules) into `Group.test.tsx`'s mocked antd-style context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ol dispatch (#14817)

* 🐛 fix(agent-runtime): honor per-tool timeout end-to-end for client tool dispatch (LOBE-8436)

Server BLPOP was hardcoded to 60s and ignored the LLM-supplied `timeout` in
`tool_call.arguments`, so long-running shell commands consistently failed
with a server-side timeout while the desktop runner was still happily
executing. Renderer also never raced its own deadline, leaving it free to
hang past the server budget.

Plumb a per-tool timeout through the full chain:

  - New `resolveToolTimeoutMs` (server) — priority: `args.timeout` >
    `manifest.api[apiName].defaultTimeoutMs` > 120s global default,
    clamped to [1s, 800s] (cloud function ceiling).
  - `dispatchClientTool` accepts `timeoutMs` in ctx; constants moved into
    `resolveToolTimeout.ts`. Default 60→120s, max 270→800s.
  - `RuntimeExecutors` calls the resolver at both client-dispatch sites
    (single + batch) using the LLM-parsed args and the effective manifest.
  - `LobeChatPluginApi` (types + context-engine) gains
    `defaultTimeoutMs?: number` so tool authors declare per-API budgets.
  - `LocalSystemManifest` sets per-API defaults: runCommand 120s,
    read/write/edit/list 30s, grep/glob/search/move 60s, killCommand 10s.
  - `local-file-shell/runner.ts` internal kill cap raised 600→800s to
    match the server ceiling.
  - Renderer `clientToolExecution.ts` rewritten to (1) race executor
    against `executionTimeoutMs - 500ms`, abort the operation's
    AbortController, and send `client_executor_timeout` on overrun;
    (2) read `gatewayConnections[operationId]` live on every send so
    reconnects between dispatch and result are picked up; (3) wrap in
    try/finally with an exactly-once `sent` guard so every `tool_execute`
    yields exactly one `tool_result` even on logic gaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(test): drop unused @ts-expect-error and tighten timeout assertion

CI lint failed on tsgo: an `@ts-expect-error` directive in
`resolveToolTimeout.test.ts` was unused (the field's `unknown` value
type happily accepts a string at compile time), and the
`sendToolResult.mock.calls[0][0]` access in `clientToolExecution.test.ts`
tripped TS2493/TS2532 because vitest typed `calls` as an empty tuple.

Cast the test-only string value through `unknown` for the resolver
defense check; merge the budget assertion into the `toHaveBeenCalledWith`
matcher via `expect.stringContaining('2000ms')` so we never index into
`mock.calls` by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o /client (#14888)

* 🐛 fix(local-system): forward all grepContent params + move executor to /client

The local-system executor was reducing the agent's full grepContent params
({pattern, glob, output_mode, -i/-n/-A/-B/-C, multiline, head_limit, type,
scope, ...}) down to {directory, pattern} before handing them to the runtime.
`directory` isn't recognized by the IPC layer (which expects path/scope), so
cwd silently fell back to process.cwd() (= apps/desktop/ in dev), and with
glob/-i/output_mode all stripped grep matched anything containing the pattern
across the whole tree — explaining LOBE-8666's dist/main/index.js +
tsconfig.tsbuildinfo leaks.

Also audited the rest of the executor layer:
- listFiles: forward `limit` (was silently dropped → manifest default of 100
  always won).
- getCommandOutput: forward `filter` (was silently dropped → no regex filter
  ever applied to streamed output).
- runCommand: mirror `run_in_background` → `background` so
  ComputerRuntime.RunCommandState.isBackground reflects reality (the IPC
  handler reads run_in_background directly, so the command itself ran in
  background — only the state field was wrong).

Structure: moved src/executor/ → src/client/executor/ to match the other
builtin-tool packages (task / lobe-agent / knowledge-base) and consolidate
renderer-only code under /client. Dropped the `./executor` package subpath;
consumers now import from `…/client`.

Defensive: also added a resolveSearchPath helper in apps/desktop's
contentSearch module that reads params.scope as a fallback for params.path,
so any non-executor caller (direct IPC, future Gateway path) that passes
`scope` still gets routed correctly instead of falling through to
process.cwd().

Regression coverage:
- grepContent full forwarding (LOBE-8666 case + all optional flags)
- listFiles.limit forwarding
- getCommandOutput.filter forwarding
- runCommand.run_in_background → background mirror
- resolveSearchPath fallback semantics (3 cases in base.test.ts)

Verified end-to-end via Electron CDP — tool.invokeBuiltinTool with the
LOBE-8666 params returns 9 clean .ts matches (no dist/, no .tsbuildinfo);
listFiles {limit:3} returns 3 files (totalCount 10); runCommand
{run_in_background:true} reports state.isBackground=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): readFile fails with `protocol.registerSchemesAsPrivileged should be called before app is ready`

Two-part fix for a regression where reading any text/JSON/source file via the
local-system `readFile` tool surfaced an Electron protocol error in the response
content. The error fired *after* `stat()` succeeded (so missing-file ENOENT was
unaffected), making it look like the file couldn't be parsed.

## Root cause

Stack trace (instrumented `read.ts` to capture it):

```
Error: protocol.registerSchemesAsPrivileged should be called before app is ready
    at new App (apps/desktop/dist/main/index.js:105339:21)
    at Module.<anonymous> (apps/desktop/dist/main/index.js:105615:11)
    at Module._compile (...)
```

`Module._compile` on `dist/main/index.js` means the main bundle is being freshly
evaluated as a CJS module — re-running its top-level `var app = new App(); …;
app.bootstrap();` after the real Electron-launched App was already ready.

Triggering chain: agent calls `readFile` → main runs `loadFile(path)` from
`@lobechat/file-loaders` → `getFileLoader('txt')` → `await import('./text')`.
The lazy text-loader chunk back-references the main bundle for the shared util
`detectUtf16NoBom`:

```js
// dist/main/text-Cbmlmtca.js
const require_index = require("./index.js");      // ← re-evaluates main
…
const variant = require_index.detectUtf16NoBom(buffer);
```

Electron's main entry is not in Node's CJS module cache (it's bootstrapped
separately), so this `require("./index.js")` triggers a fresh compile of the
main bundle — re-running `new App()` and `protocol.registerSchemesAsPrivileged`
*after* `app.whenReady()`, which is illegal per Electron's API contract.

Introduced by #14602 (`fix(local-system): guard readFile against binary blobs
and oversized output`): adding `isBinaryContent.ts` made `detectUtf16NoBom`
shared between the main bundle (via `sniffBinaryFile`) and the lazy text chunk,
so rolldown placed it in main and rewrote the text chunk's call as a
`require_index.detectUtf16NoBom`.

Identical class of bug previously fixed for the `debug` package in #11827.

## Fix

1. **`packages/file-loaders/src/loaders/index.ts`** — TextLoader was lazy-imported
   for no real benefit. It's a 10KB module whose only deps are `node:fs/promises`
   and a tiny utf-16 detect util — nothing like the multi-MB parsers (pdfjs-dist,
   xlsx, mammoth) that the lazy pattern was designed for. Make it a static
   import; `getFileLoader('txt')` returns it synchronously. Result: the text
   chunk disappears entirely, removing this back-reference at the source.

2. **`apps/desktop/electron.vite.config.ts`** — defensive `manualChunks` rules
   so any future shared symbol doesn't recreate the same trap:
   - `vendor-file-loaders-utils` for the three small text/binary detection
     utils (`detectUtf16` / `isBinaryContent` / `isTextReadableFile`).
     Explicitly enumerated to avoid catching `parser-utils.ts`, which pulls
     in xmldom/yauzl/concat-stream (≈900KB) and belongs in the docx/pptx
     chunks instead.
   - `vendor-jszip` for JSZip — same root cause for `.docx` reads: the docx
     chunk had `require_index.require_lib()` (JSZip) back-referencing main.
     Both ends now share the vendor chunk; no main re-eval.

Follows the project precedent set by #11827 for `debug`.

## Verification (live Electron via CDP)

Bundle inventory before/after:

| Chunk | Before | After |
| --- | --- | --- |
| `text-*.js` | 9.7KB (back-refs main) | (gone, inlined into main) |
| `vendor-file-loaders-utils-*.js` | n/a | 18KB |
| `vendor-jszip-*.js` | n/a | 899KB |
| `docx-*.js` back-refs | `require_index.require_lib` | none |

End-to-end via `tool.invokeBuiltinTool('lobe-local-system', 'readFile', …)`:

| File | Before | After |
| --- | --- | --- |
| `.md` / `.json` / `.ts` | `Error accessing or processing file: protocol.registerSchemesAsPrivileged should be called before app is ready` | real file content |

`grep -o 'require_index\\.[a-zA-Z_]*' dist/main/*-*.js | sort -u` → empty.

All 61 file-loaders tests pass; all 64 builtin-tool-local-system tests pass.
…14292)

* ✨ feat: agent-documents index — hide web crawls + new table format

The default `<agent_documents_index>` was injecting every progressive
document — including hundreds of web-crawled snapshots (~73% of all
agent docs in production). The result was a low-signal list dominated
by duplicate page titles, plus zero metadata for the LLM to rank by.

This revamp:

- Hides `source_type=web` documents from the default index. Header
  surfaces the count and points the LLM at `listDocuments(sourceType=
  'web')` to enumerate them when needed.
- Renders the index as a fixed-width table with TITLE / ID / SIZE /
  UPDATED columns. Rows are sorted by recency (most-recent first).
  Empty docs render as `empty` to discourage retry reads.
- Adds `sourceType` and `updatedAt` to the `AgentContextDocument`
  contract; client mapping populates both from the DB row.
- Adds `sourceType: 'all' | 'file' | 'web'` parameter to the
  listDocuments tool/TRPC; service-layer filter applies before
  shaping the LLM response.
- Renames `target` → `scope` on listDocuments + createDocument
  (manifest, types, runtime, system role, TRPC, client service,
  call sites, tests). `target="currentTopic"` becomes
  `scope="currentTopic"` everywhere.

Coverage: inline snapshot tests in
`packages/context-engine/src/providers/__tests__/AgentDocumentInjector.test.ts`
pin the rendered output for the three load cases (mixed user docs,
web-hidden header, empty doc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(test): update listDocuments mock assertion for sourceType default

The agent-documents listDocuments runtime now forwards sourceType
(defaulting to 'all'), so the spy receives two positional args.

* 📝 docs(builtin-tool-local-system): bump documented runCommand max timeout to 800000ms

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fix (#14898)

FullNameStep is the classic branch's first step; its back button called
goToPreviousStep, which no-ops at step 1 — a dead link ever since the
telemetry/language steps were extracted into the shared prefix.

Route it back to ResponseLanguageStep, and let CommonOnboardingPage
re-enter the shared prefix when an explicit `?step` is present (a bare
`/onboarding` still resumes the branch).
…preview (#14896)

* ✨ feat(hetero-cc): surface project skills in working sidebar + markdown preview

When the active agent is a heterogeneous Claude Code session, the Space tab
now lists skills discovered under `<cwd>/.agents/skills/` (with a fallback
to `<cwd>/.claude/skills/`). Each row shows the skill's frontmatter name,
file count, and a chevron to expand a peek at the bundle contents; clicking
the name opens `SKILL.md` in the LocalFile portal, and clicking a child
file opens that file directly.

The LocalFile portal also gets a Preview / Raw toggle for `.md` / `.mdx`
files — frontmatter is now parsed and the YAML block stripped from the
rendered markdown body (no more `name: x description: y` reading as a wall
of body text). The portal tab strip distinguishes SKILL.md tabs by showing
the skill name with the Skills icon instead of the generic filename, and
falls back to a file icon for all other open files. Markdown content gets
its own scroll container so the Preview pane scrolls correctly.

The space-tab AgentDocuments group is hidden for hetero CC sessions so the
panel focuses on skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(hetero-cc): default to Space tab for hetero sessions

Hetero CC right-panel now defaults to the Space tab (where the Skills
module lives) when there's no prior stored tab choice. Non-hetero sessions
keep the existing review/files/resources fallback order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(hetero-cc): surface cumulative progress on Task inspector rows

TaskCreate / TaskUpdate-with-status inspector rows now lead with the
same ProgressRing (from pluginState.todos) and a `completed/total`
chip, so a mixed create/update column reads as one continuous progress
gauge instead of bare-text per-row signals. The verb in the label
still carries the per-row status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(hetero-cc): project skills in slash menu + skills panel polish

Surfaces `.agents/skills/` SKILL.md entries as a new `projectSkill`
ActionTag category in the chat input's `/` menu so users can invoke
project skills the same way CC does internally. The chip serializes to
literal `/<skill-name>` on send, leaving CC's own skill resolution
untouched (no system prompt injection).

Side-panel polish bundled in: the Space-tab Skills list expands as a
real directory tree, the LocalFile portal renders SKILL.md frontmatter
as a metadata card (reusing parseSkillMarkdownMetadata), and skill rows
use the secondary→colorText hover pattern. Also passes `data.root` (the
exact root listProjectSkills approves) to openLocalFile so previews
never hit the workspace-root mismatch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fix multi-replica parent_id breaks (#14897)

* 🐛 fix(hetero-agent): restore tools/model from DB at ingest refresh to fix multi-replica parent_id breaks

In prod a topic with 11 step boundaries produced 4 assistants whose
parentId pointed at the previous assistant instead of the previous tool
message — same in-memory state.toolState gets reset at the end of every
handleStepStart, so if the next step's tools_calling lands on a different
replica, this replica stays empty and the following step boundary falls
back to currentAssistantMessageId. Two of the four also had
model=null/provider=null for the same reason: handleTurnMetadata only
cached lastModel/lastProvider in memory.

Adopt DB as authoritative at the ingest() refresh: replace
state.toolState wholesale when DB has more tools or more result_msg_ids
than memory, and restore state.lastModel/lastProvider from the refreshed
assistant row. Also extend handleTurnMetadata to persist model/provider
to DB (previously only metadata.usage was written), so the refresh path
has something to recover from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): never mark unresolved restored tools as persisted

Three sites that hydrate `state.toolState` from DB-side `assistant.tools[]`
were unconditionally pushing every id into `persistedIds`:

- `ingest()` refresh (newly added in the prior commit on this branch)
- `loadOrCreateState` (cold replica boot)
- `syncAssistantPointerForAdvancedStep`

`persistToolBatch` writes `tools[]` in Phase 1 BEFORE creating the
`role:'tool'` row in Phase 2 and backfilling `result_msg_id`. A replica
that hydrates between those two phases sees an unresolved id; marking it
as persisted then causes a follow-up retry of the same tools_calling
event to fall out of `freshForCreate`, skip Phase 2, and rewrite the
unresolved `tools[]` unchanged — leaving the tool permanently without a
tool message / result_msg_id.

Restore only ids whose `result_msg_id` is already set. Unresolved ids
stay re-createable so the BatchIngester's outer retry can complete the
write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…14883)

* 🐛 fix(agent): stop auto-collapsing right working panel on chat mount

ChatConversation had a mount effect that forcibly toggled showRightPanel
off whenever status init completed, so switching to a new topic (which
remounts the route subtree) would close the user's Workspace panel.
Drop the effect and default showRightPanel to false instead — the
persisted user preference is now the single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): keep right-panel toggles usable before status hydration

INITIAL_STATUS.showRightPanel now defaults to false, which means
WorkingPanelToggle / ToggleRightPanelButton / ParamsPanelToggle render
their "open" button during the pre-hydration window. But
updateSystemStatus bails early while isStatusInit is false, so the very
first click was silently dropped and the panel stayed closed even after
hydration when storage was empty.

Defer rendering these toggles until isStatusInit flips true so a click
can never land in the no-op window. Also fix the
action.test.ts > toggleRightPanel > should toggle chat sidebar case,
which was passing only because the old default was true; it now hydrates
the store before asserting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): stop overwriting working-sidebar tab when reopening panel

WorkingPanelToggle unconditionally set storedTab='review' on every
click, so any Space/Files preference the user had clicked previously
got clobbered the next time they re-opened the right panel — most
visibly on hetero CC sessions where the intended default is Space.

The toggle now just toggles the panel open; the sidebar's own
resolveActiveTab handles defaulting (hetero → Space, otherwise → last
explicit click, then Review/Files based on local-system availability).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… skill menu (#14901)

* ♻️ refactor(chat-input): adopt native submenu header/footer slots for skill menu

The skill menu in the Plus dropdown pinned its search bar and stats footer as faux menu items held by position:sticky CSS hacks (data-fixed-menu-footer / data-skill-menu-search / data-skill-stats). @lobehub/ui 5.14.0 adds native header/footer slots to submenu popups, so move the search bar and stats row onto those slots and drop the hacks.

* ♻️ refactor(knowledge-controls): integrate footer into useControls and update PlusAction to utilize new structure

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
…not just last turn (#14904)

Previously the magic signature was only applied when the last message was a
tool message and only to functionCall parts after the last user message. This
missed cross-provider scenarios (e.g. OpenAI GPT-5 → Gemini switch) where
historical tool_calls lack thoughtSignature, causing Gemini API warnings:

  Function call is missing a thought_signature in functionCall parts.

Now we unconditionally iterate all model-role contents and add the magic
signature to any functionCall part that doesn't have one, ensuring Gemini's
thought signature validator is always satisfied regardless of conversation
history origin.

See LOBE-8662
…14903)

* 💄 style(chat-input): switch action tag chips to icon + colored label

Replace the filled Tag chip with an inline icon + colored label so skill
and command references read like prose instead of UI badges.

- Use SkillsIcon for skill / projectSkill (both green via colorSuccess)
- Use TerminalIcon for command (cssVar.purple token, theme-aware)
- Use WrenchIcon for tool (cssVar.colorInfo)
- Preserve selection outline on .selected for the editor

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(chat-input): rename ActionTagView to ActionMention

The component no longer renders a Tag chip — it renders an inline icon
with colored label representing a mentioned/inserted action reference.
"Mention" matches how these are inserted in the editor (via slash menu or
@-mention) and reads better in the user-message renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): drop borders on @mention and @topic chips

@-mention (from `@lobehub/editor`) and @-topic refer chips both had
outlined borders; switch them to a borderless filled look so they sit
quietly inline with surrounding text — matching the new ActionMention.

- `ReferTopicView`: `variant="outlined"` → `variant="filled"`
- Add `mentionFilledClassName` (`.editor_mention { border: none }`) and
  apply it on both the editor (`InputEditor` className) and the rendered
  user message (`RichTextMessage` LexicalRenderer className) so input
  and read-back look the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(agent-sidebar): allow message channel for Claude Code hetero agents

Codex and other hetero providers still hide the channel entry; Claude Code agents can now use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): satisfy strict types for icon map and mention className

CI failures from the previous commits:

- `ActionMention` typed CATEGORY_ICON as `ComponentType<any>` which is a
  superset of `LucideIcon | FC<any> | ReactNode` accepted by `<Icon>` —
  narrow to `FC<any>` so SkillsIcon and lucide icons type-check.
- `mentionFilledClassName` was a `SerializedStyles` from `css\`\``; wrap
  in `cx()` so it serializes to a `string`, which `LexicalRenderer`'s
  `className` prop requires.
- Update `Nav.test.tsx` mock to expose the new
  `currentAgentHeterogeneousProviderType` selector that landed in 89d7515.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): keep reasoning state live during gateway streaming

The gateway event handler only accumulated reasoning text into `message.reasoning`
without ever creating a `type: 'reasoning'` operation, so `isMessageInReasoning`
was always `false`. The Thinking UI then rendered the "已深度思考" completed title
and stayed collapsed for the entire stream. Mirror `StreamingHandler`'s lifecycle:
start a reasoning sub-op on the first thinking chunk and end it on text /
tools_calling / stream_end / stream_start (next step) / agent_runtime_end / error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rMessages hotkey (#14906)

* 🐛 fix(conversation): animate only the last assistant block markdown streaming

Switch `withMarkdownStreamingState` from disabling the first block to
disabling every block except the last one. The previous logic let middle
blocks keep `animated=true` during generation, so any remount mid-stream
replayed the typewriter from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔥 chore(hotkey): remove clearCurrentMessages shortcut

Drop the Alt+Shift+Backspace binding from the chat scope. The eraser
button in ActionBar still works; only the keyboard shortcut, registry
entry, hotkey i18n and docs row are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# 🚀 LobeHub Release (20260518)

**Release Date:** May 18, 2026  
**Since v2.1.58:** 208 merged PRs · 209 commits · 16 contributors

> v2.2.0 introduces the **Chief Agent Operator** — an agent that runs
itself end-to-end. It self-iterates against its own output, assembles
sub-agent teams on demand through the heterogeneous runtime, and drives
a unified task system that knows when to pause for a human. Self-review,
AssistantGroup, and tasks/scheduling all converge into one operator
surface.

---

## ✨ Highlights

### 🎩 Chief Agent Operator

- **Self-iteration exits Lab** — Agent Signal's self-review pipeline
ships proposal actions straight into briefs and auto-executes the
approved follow-ups, with prompts hardened against eval. The operator
now critiques and re-runs its own work without a human in the loop.
(#14769, #14583, #14647, #14882)
- **Auto-formed agent teams** — Heterogeneous AssistantGroup gains
Monitor-style signal callbacks, read-only SubAgent threads with
breadcrumb headers, and a thread switcher. The operator dispatches
sub-agents and you can step into any branch to see what the team is
doing. (#14859, #14658, #14845, #14715)
- **Task system as the operator's runway** — Claude Code surfaces task
tools, AskUserQuestion freeform notes, and a dedicated `waitingForHuman`
topic status; `lobe-task` exposes `setTaskSchedule`; the scheduler is
hardened (maxExecutions cap, sub-10min heartbeat block, race-free
SchedulerForm). Long-running operator runs no longer go silent and stop
themselves when human input is needed. (#14870, #14639, #14713, #14865,
#14853)

### 🚀 Cloud & runtime

- **Cloud Claude Code V3** — Repo picker, GitHub token flow, and
sandbox-aware context bring cloud-hosted Claude Code to feature parity
with local; cloud sandbox completion now triggers the task lifecycle
end-to-end. (#14568, #14822, #14681)
- **Heterogeneous agent multi-replica safety** — Subagent threads,
ingest refresh, and parallel-tool counts now survive replica swaps
without losing parent_id or rolling back tool state. (#14897, #14631,
#14806, #14838)
- **Built-in tool lifecycle hooks** — `onBeforeCall` / `onAfterCall`
land on the built-in tool runtime; sub-agent dispatch moves to
`lobe-agent`; self-iteration aligns with the shared inspector pattern.
(#14719, #14715, #14827)
- **Knowledge base RAG unified** — Client and server share one
`KnowledgeBaseSearchService`; KB files preserved on `NoSuchKey` instead
of silently lost. (#14673, #14501)

### 💬 Workspace experience

- **Home daily brief + recommendations** — The home screen opens with a
linkable welcome, paired input hint, and a recommendations module
sourced from the operator's hetero action library. (#14589, #14645,
#14770)
- **Chat mode + redesigned action bar** — The chat input gains a
Chat/Agent mode toggle and a re-pitched action bar with icon-and-color
action tag chips. (#14774, #14903, #14846)
- **Documents tree, optimistic** — Document tree creates, deletes, and
inline renames now apply optimistically; the agent-documents index hides
web crawls and switches to a table layout. (#14714, #14292)
- **Branded MCP inspectors** — Linear MCP tool calls render with the
same branded inspector as the built-in Linear skill; CC MCP and built-in
skills now share inspector code. (#14864, #14884)
- **Bot identity gating** — Device tools are gated by sender identity,
the activator bypass is closed, and Slack mpim plus Discord DM
regressions are fixed. (#14634, #14664, #14733)

---

## 🏗️ Core Agent & Signal Pipeline

### Self-iteration & Agent Signal

- Self-iteration graduates out of Lab, with service, tool, name, and
concept structure unified across `agent-signal`, `prompts`, `database`,
and `builtin-tool-self-iteration`. (#14699, #14769)
- Self-review now proposes actions to briefs and auto-executes the
approved set, with eval-verified prompt hardening. (#14583, #14657,
#14647)
- Self-iteration built-in tool aligns with the shared runtime +
inspector patterns. (#14827)
- Agent Signal prompts adapt their response language and avoid blocking
agent execution. (#14890, #14775, #14882)
- Receipt descriptions now carry an Agent Signal marker, and self-review
hinted skill documents route correctly. (#14764, #14895)

### Heterogeneous agent runtime

- Subagent threads render read-only with a breadcrumb header and thread
switcher; SUBAGENT badge dropped, indentation tightened. (#14658,
#14845, #14783)
- Multi-replica safety: ingest refresh restores tools/model from DB to
fix parent_id breaks; new-step assistants sync across replicas;
subagent-tagged events no longer leak into the main gateway handler.
(#14897, #14631, #14838)
- Fetch-triggering events are deferred to keep parallel tool counts from
rolling back. (#14806)
- AskUserQuestion is wired for Claude Code, with auto-decline disabled
and a freeform note input on the cloud side; `waitingForHuman` is a
first-class topic status. (#14639, #14629, #14870)
- AssistantGroup gains Monitor-style signal callbacks; project skills
surface in the working sidebar and markdown preview. (#14859, #14896)
- Cloud Claude Code V3 — repo picker, GitHub token, sandbox context;
credentials alert and disabled input when not configured. (#14568,
#14822)
- Cloud sandbox completion now triggers the task lifecycle end-to-end.
(#14681)

### Agent runtime & context engine

- Built-in tool runtime gets `onBeforeCall` / `onAfterCall` lifecycle
hooks. (#14719)
- `CompletionLifecycle`, `HumanInterventionHandler`, and
`stepPresentation` are extracted from the runtime monolith. (#14441)
- Per-tool timeout is honored end-to-end for client tool dispatch.
(#14817)
- Compression budget accounts for `tool_calls`, reasoning content, and
tool defs; `call_llm` forwards tools into the budget. (#14813, #14837)
- Pre-flight context check now fails fast for OpenAI-compatible
providers. (#14824)
- Malformed `tool_call` names are recovered instead of finishing the
step silently. (#14577)
- Sub-agent dispatch moves from `lobe-gtd` to `lobe-agent`. (#14715)
- Hidden built-in tools now appear in the system prompt @-mention list.
(#14823)

### Agent tracing & operations

- New `agent_operations` table and runtime persistence for every
hetero-agent operation. (#14416, #14736)
- `signOperationJwt` issues 4-hour signed operation tokens. (#14586)
- S3 trace snapshots are zstd-compressed; DB `trace_s3_key` aligns with
the `.json.zst` suffix; legacy `.json` fallback preserved on fetch.
(#14807, #14860, #14826)

---

## 📱 Platform & Integrations

### Bot / Channels

- Device tools are gated by sender identity. (#14634)
- Activator bypass closed and device-access checks converged. (#14664)
- Slack mpim supported; Discord DM regression fixed; Slack connect +
slash commands repaired. (#14733, #14591)
- Bot channels, bot watch, bot callback service, and system bot
reliability fixes. (#14847, #14796, #14570, #14784, #14649)
- Online Messager scaffolding. (#14755)

### Onboarding

- Home daily brief with linkable welcome and paired input hint. (#14589)
- Recommendations module sourced from the hetero agent action library.
(#14645)
- Chat onboarding passes request triggers via metadata and preserves the
resume request. (#14770, #14798)
- Discovery turn progress gated by phase, with a reminder on stalled
discovery. (#14842, #14833)
- FullNameStep back button rejoins the shared prefix; ModeSwitch hidden
in production. (#14898, #14760)
- Agent marketplace folds into the web onboarding tool. (#14578, #14672)
- Onboarding interests stored as keys instead of free text; early-exit
skips marketplace and drops CJK prompts. (#14624, #14598)

### Model providers

- Gemini 3.1 Flash-Lite cards; Gemini schema sanitizer drops
non-compliant `enum` / `required`; zero `cachedContentTokenCount`
handled in usage conversion. (#14604, #14740, #14567)
- DeepSeek-V4 model cards and pricing restored to official rates.
(#14110, #14911)
- ernie-5.1 and spark-x2-flash support; Grok 4.3 `reasoning_effort`
support. (#14643, #14731, #14642)
- SiliconCloud catalog synced with API; duplicates removed; reasoning
params adjusted. (#14464)
- Minimax derives `max_tokens` from context window to avoid
`ExceededContextWindow`. (#14814)
- aihubmix uses the full models endpoint for a complete list; stale
empty-apiKey test dropped. (#14511, #14669)
- Stream parse errors are enriched with provider + model context.
(#14636)
- Visual content parts are consumed in the server runtime; video image
references move to a JSON object. (#14637, #14900)
- Google function call magic `thoughtSignature` now attaches to every
part, not just the last turn. (#14904)
- Service model assignments settings added; model extend-param options
removed. (#14712, #14607)

### Built-in tools & knowledge base

- `lobe-task` exposes `setTaskSchedule`; task scheduler hardened
(maxExecutions cap, sub-10min heartbeat blocked, SchedulerForm race fix,
rapid automation-mode toggle stabilized). (#14713, #14865, #14853,
#14801)
- KnowledgeBaseSearchService shares RAG runtime across client and
server. (#14673)
- KB files preserved on `NoSuchKey` and orphan documents/tasks cleaned.
(#14501)
- Document tree gets optimistic create/delete + inline rename. (#14714)
- agent-documents index hides web crawls and switches to a table layout.
(#14292)
- `lobe-clarify` and SKILL.md frontmatter parsing/edit validation are
unified. (#14566)
- AnalyzeVisualMedia inspector + Portal HTML preview refactor; HTML
preview restored for AssistantGroup messages. (#14777, #14811)
- Branded inspector shared between CC MCP and built-in Linear skill.
(#14884, #14864)

---

## 🖥️ CLI & User Experience

### Chat & Conversation

- Chat mode toggle and redesigned chat input action bar. (#14774)
- Action tag chips switch to icon + colored label; ActionDropdown closes
on sibling-open and focus-out; submenu uses native header/footer slots.
(#14903, #14802, #14901)
- Action bar padding equalized around the send button; skeleton shows in
action bar while config loads. (#14846, #14656)
- `useCmdEnterToSend` is respected in thread & task inputs; send button
enables after pasting into thread/comment input. (#14850, #14816)
- TopicChatDrawer state preserved during close animation. (#14803)
- Only the last assistant block animates during markdown streaming.
(#14906)
- Right working panel no longer auto-collapses on chat mount; home agent
config fetched so knowledge toggles reflect in UI. (#14883, #14834)

### Tasks

- Task scheduler, hotkey, comment, and TodoList polish. (#14707)
- Add Subtask button & card baseline aligned; activity card stop run;
task agent manager polish. (#14848, #14559, #14569)
- Task template skeleton CLS reduced; task page placeholder copy
refreshed. (#14788, #14704)
- Task agent model snapshotted into `task.config` at create time.
(#14670)
- User-feedback card, task card polish, and Run-now context menu in
markdown. (#14727)
- Inline skill auth in recommended task templates. (#14676)

### Navigation & Layout

- Tab bar gains a Chrome-style divider between inactive tabs. (#14892)
- SideBarDrawer & header layout polish; nav ActionIcon sizing unified;
TodoList encapsulation improved. (#14762, #14692)
- Desktop header icons, sidebar density, and task menus polished.
(#14724)
- Standardized header action icon sizes. (#14717)
- Chat topic title length increased; copy session ID added to topic
dropdown menu. (#14659, #14595)
- Heterogeneous agent topic rows regain indentation. (#14783)

### Other polish

- Usage token details shortened; tool execution time formatted as `Xmin
Ys`. (#14849, #14641)
- Tool arguments display gets word-wrap toggle; long tool-call params
wrap instead of truncate. (#14706, #14640)
- Editor stops showing per-line placeholder once content is present.
(#14852)
- Visible divider between queued messages; intervention confirmation bar
polished. (#14593, #14587)
- Credit top-up copy refreshed; auth captcha retry copy refreshed; brief
recommendations layout polished. (#14821, #14561, #14871)

---

## 🔧 Tooling & Developer Experience

- Dev-only feature flag override panel. (#14565)
- `__DEV__` define replaces `process.env.NODE_ENV` in the SPA. (#14696)
- Agent-settings drops Meta/Documents tabs and restores `inputTemplate`.
(#14874)
- `local-system` forwards all `grepContent` params and moves the
executor to `/client`. (#14888)
- `lobe-task` and `setTaskSchedule` exposed. (#14713)
- Memory user-memory benchmark agent config and source-id extraction
schemas. (#14779, #14778)
- CLI man page drops stale cron entry; `clearMessages` hotkey removed.
(#14709, #14906)
- Skill docs simplified; cloud heteroContext gains sandbox TTL +
public-repo fork push guide. (#14785, #14761)

---

## 🔒 Security & Reliability

- **Security:** Sensitive comments and examples sanitized from the
production JS bundle. (#14557)
- **Security:** Inactive OIDC access rejected. (#14674)
- **Security:** CASC `new Function()` template replaced with safe string
builders. (#14751)
- **Security:** Sign-in captcha flow removed in favor of safer flow.
(#14573)
- **Security:** Desktop local file previews restricted to safe roots.
(#14789)
- **Security:** Image binary capped at 3.75 MB so base64 payload stays
under the Anthropic 5 MB limit. (#14711)
- **Reliability:** Neon/Node pools get error listeners to prevent Lambda
crashes. (#14606)
- **Reliability:** `paradedb.match(...)` replaces hardcoded normalizer
in memory search. (#14590)
- **Reliability:** `PlaceholderVariablesProcessor` errors carry
diagnostic context. (#14741)
- **Reliability:** File storage upload checks are serialized; multiple
account link bug fixed. (#14829, #14562)
- **Reliability:** `ScrollShadow` replaced with `ScrollArea` to fix a
React infinite render loop (error code 185). (#14689)
- **Reliability:** Embedding token cap enforced — long memory queries
are limited and truncated before search. (#14757)
- **Reliability:** Embed binary blob guard + oversized output cap in
`local-system.readFile`. (#14602)
- **Reliability:** Windows npm CLI shims resolved before spawning
agents. (#14772, #14720)
- **Reliability:** Vite pinned to 8.0.12 to avoid the rolldown 1.0.1
preload regression; desktop runtime externals split from native deps.
(#14804, #14776)
- **Reliability:** Old lobehub cron job removed; WeChat URL rules
dropped from web crawler. (#14630, #14633)

---

## 👥 Contributors

Huge thanks to **16 contributors** who shipped **208 merged PRs** this
cycle.

@hezhijie0327 · @sxjeru · @hardy-one · @Bianzinan · @brone1323 · @YuSaZh
· @Wxh16144 · @arvinxx · @Innei · @tjx666 · @neko · @lijian · @rdmclin2
· @sudongyuer · @AmAzing129 · @rivertwilight

Plus @lobehubbot for maintenance translations.

---

**Full Changelog**:
v2.1.58...v2.2.0
#14923)

The schedule pill (TaskTriggerTag in tag mode) had a fixed 24px height
but no single-line constraint on its inner Text, so long descriptions
like "每周 日/一/二/六 09:00 运行" wrapped to two lines and broke the
row layout in the Kanban card. Force single-line + ellipsis truncation
and let the existing tooltip surface the full string + timezone.

Also hoist inline style objects to module scope so React.memo on
Block/Flexbox/Text isn't defeated as the Kanban re-renders many cards.

Fixes LOBE-9149

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arvinxx and others added 30 commits May 27, 2026 23:18
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ommon (#15255)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: telegram messager installation

* fix: lint error

* fix: telegram resolve Credentials first
…istry (#15262)

* ✨ feat(model-runtime): unify error codes into spec + pattern registry

Add a single source of truth for runtime error classification under
`packages/model-runtime/src/errors/`:

- `taxonomy.ts` — category / severity / attribution dimensions
- `specs.ts` — ERROR_CODE_SPECS: per-code httpStatus / retryable /
  countAsFailure / attribution (user | provider | harness | system)
- `patterns.ts` — ERROR_PATTERNS: substring/regex registry consolidating
  the 5 separate isXxxError lists and the upstream provider message
  patterns previously kept only in agent-gateway
- `match.ts` — matchErrorPattern() + isUserSideError()

Wire-up:
- Add 8 codes to AgentRuntimeErrorType (ProviderServiceUnavailable,
  ProviderNetworkError, NoAvailableChannel, ContentModeration,
  CapabilityNotSupported, InvalidRequestFormat, UserConfigError,
  OperationInactivityTimeout) plus their en-US locale keys
- Rewrite isExceededContextWindow / isQuotaLimit / isInsufficientQuota /
  isAccountDeactivated as one-line wrappers around matchErrorPattern
- errorResponse.ts getStatus() now reads ERROR_CODE_SPECS, removing the
  hardcoded switch

Tests: 167 model-runtime test files (3916 pass / 1 skip) including 13
new match.test.ts cases and all 42 isXxxError snapshots unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(model-runtime): add numericId (E1001) + ErrorClassifier namespace

Numeric reference codes for external surfaces (open-source consumers, docs
anchors, support tickets):

- `ErrorCodeSpec.numericId` (required, 4-digit). Append-only contract: once
  assigned, a (code, numericId) pair never changes even if the string `code`
  is renamed.
- Format: `E<numericId>` (e.g. `E1001` InvalidProviderAPIKey, `E3001`
  QuotaLimitReached, `E7002` OperationInactivityTimeout).
- First digit encodes category via `CATEGORY_NUMERIC_PREFIX`:
  1=auth, 2=quota, 3=capacity, 4=request, 5=safety, 6=network, 7=stream,
  8=provider, 9=config.
- Helpers: `formatErrorRef(code) → 'E1001'`, `parseErrorRef('E1001') → code`.
- Test guards: numericId is unique across specs; leading digit matches the
  declared category for every entry.

Consolidate classification predicates:

- New `ErrorClassifier` namespace bundles `isExceededContextWindow` /
  `isInsufficientQuota` / `isQuotaLimitReached` / `isAccountDeactivated`
  behind a single discoverable import.
- The 4 scattered `is*Error.ts` utilities are now `@deprecated`; kept as
  shims for callers that aren't migrated yet.
- Parity test asserts ErrorClassifier and the legacy utils return the same
  boolean on a curated sample set.

Tests: 168 files / 3928 pass / 1 skip. +12 new tests for numericId contract,
ref formatting, and classifier parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): rename QuotaLimitReached → RateLimitExceeded

The legacy name conflated two distinct semantics: short-window rate limit
(429-class, transient, retryable, provider-attributed) vs. long-term
account-level quota exhaustion (`InsufficientQuota`, user-attributed).
Surface code readers hit this confusion the moment they look at the spec
table — the name reads like a 2xxx quota code but the spec sits in 3xxx
capacity.

- Add `AgentRuntimeErrorType.RateLimitExceeded` as the canonical name.
- Keep `AgentRuntimeErrorType.QuotaLimitReached` as a `@deprecated` alias
  (same string value preserved for legacy stored data on the dashboard
  side) — `CODE_ALIASES` map in `specs.ts` ensures `getErrorCodeSpec` /
  `isUserSideError` resolve both old and new strings to the canonical
  E3001 spec.
- `ErrorClassifier`: new `isRateLimitExceeded` is canonical;
  `isQuotaLimitReached` kept as deprecated alias.
- Refresh patterns.ts (~24 entries) + isQuotaLimitError util.
- Locale: add `response.RateLimitExceeded` next to the kept legacy
  `response.QuotaLimitReached`.
- Match.ts now reads via `getErrorCodeSpec` so alias resolution flows
  through one place.

Tests: 3930 model-runtime tests pass (+2 explicit alias-resolution cases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(AgentRuntime): wire classifyLLMError to ERROR_CODE_SPECS

The runtime retry loop's STOP_ERROR_TYPES was a hardcoded set that didn't
move with the unified error scheme. New codes added in #15262
(ContentModeration, InvalidRequestFormat, UserConfigError, NoAvailableChannel,
OperationInactivityTimeout, CapabilityNotSupported, LocationNotSupportError,
ExceededToolLimit, …) all carry `retryable: false` in the spec, but an
error arriving with one of these `errorType` values **and no HTTP status**
(e.g. a gateway-classified moderation message like "Content Exists Risk")
fell through to the classifier's default `retry` branch, producing pointless
retry storms for requests the spec says should stop.

Fix:

- Derive `STOP_ERROR_TYPES` / `RETRY_ERROR_TYPES` from `ERROR_CODE_SPECS` at
  module load. Future codes added to the spec table now classify
  automatically — no second source of truth.
- Keep a tight `RETRY_OVERRIDES` set for the 4 legacy codes
  (`AgentRuntimeError` / `OllamaServiceUnavailable` / `ProviderBizError` /
  `StreamChunkError`) that the runtime intentionally retries even though
  the spec marks them non-retryable; these are catch-all / harness-level
  failures often transient in practice.
- Resolve through `getErrorCodeSpec` before set lookup so the deprecated
  `QuotaLimitReached` alias classifies the same as its canonical
  `RateLimitExceeded`.
- Export the `errors/` module from `@lobechat/model-runtime` root barrel.

Tests: 31 cases (+12) including `it.each` coverage of all 8 newly-stop
codes and 3 newly-retry codes, plus explicit guards for the legacy retry
overrides and the QuotaLimitReached → RateLimitExceeded alias.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): consolidate isXxxError utils into ErrorClassifier

Three structural cleanups on top of the unified error scheme:

1. **Reorder `ERROR_CODE_SPECS` strictly by `numericId`.** Previously the
   spec table followed the original loose category groupings, which left
   stragglers like `InvalidOllamaArgs` (E9001, config) wedged into the
   1xxx auth section. Now entries appear in 1001 → 9005 order with
   numeric-prefix section dividers. Added `it('spec entries appear in
   source order sorted by numericId')` as a lint guard so future
   additions stay sorted (JS preserves object-literal insertion order).

2. **Migrate all production callers from `isXxxError` utils to
   `ErrorClassifier` namespace.** Touched 4 files, 13 call sites:
   - `core/anthropicCompatibleFactory/index.ts` (6)
   - `core/openaiCompatibleFactory/index.ts` (4)
   - `providers/bedrock/index.ts` (1)
   - `utils/googleErrorParser.ts` (2)

3. **Delete the 4 deprecated util files + their tests.** With no
   production callers left, the shim layer is dead code. Classifier
   tests now stand on their own (no parity comparison against the
   deleted utils).

Also mirror the spec ordering to `agent-gateway/src/errors/specs.ts`
(separate commit on that repo).

Tests: 164 files / 3908 pass / 1 skip (was 168 / 3930 — the delta is the
4 removed `isXxxError.test.ts` files, ~42 tests, net of new classifier
coverage).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(AgentRuntime): stub ERROR_CODE_SPECS in test mocks of @lobechat/model-runtime

`classifyLLMError` now reads `ERROR_CODE_SPECS` + `getErrorCodeSpec` at
module-load time to derive the STOP / RETRY sets. Two test suites mock
`@lobechat/model-runtime` sparsely (only `consumeStreamUntilDone` or
`getModelPropertyWithFallback`), so those new exports were undefined and
the module-eval crashed with `No "ERROR_CODE_SPECS" export is defined on
the "@lobechat/model-runtime" mock`.

Fix: add the two symbols to the mocks. Used empty stubs rather than
`importOriginal` so the mocks stay small and don't transitively pull
the entire model-runtime package (which would then expect every other
mocked package — e.g. `model-bank.AiModelTypeSchema` — to be complete).

Neither suite exercises the runtime retry classifier, so empty
`ERROR_CODE_SPECS` and `getErrorCodeSpec` returning `undefined` are
behaviorally equivalent to the pre-PR baseline.

Verified locally:
- `bunx vitest run src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts` — 102 tests pass
- `bunx vitest run src/server/services/agentRuntime/AgentRuntimeService.test.ts` — 60 tests pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
#15272)

* 🐛 fix(tool-archive): use .txt extension for archived tool results

Tool result content is raw output (logs, JSON, stack traces) rather than
structured markdown. Saving as .md misrepresents the format and triggers
markdown parsing downstream. Switch the archive filename to .txt to match
the actual content type.

* ✨ feat(agent-document): render non-markdown docs as readonly highlight

Agent documents whose filename does not resolve to markdown (e.g. archived
tool results saved as .txt, future .json / .yaml entries) are now rendered
through @lobehub/ui Highlighter with the inferred language, replacing the
markdown editor surface that misinterpreted raw text as syntax.

- Extract the filename→language map from FileViewer Code renderer into a
  shared util so the document viewer reuses the same mapping.
- Introduce getDocumentRenderMode: SKILL.md and .md keep the editor; all
  other extensions resolve to a Highlighter, which is naturally readonly.
- Hide the auto-save hint in Header when the document is rendered as a
  Highlighter (no editor, nothing to save).

* 🐛 fix(agent-document): render notebook documents as editor when filename is absent

Notebook documents store the markdown signal in `fileType` + `title` and never set a
`filename`. `getDocumentRenderMode` was falling back to `title` for language
inference, which resolved free-form titles like "Meeting notes" to `txt` and routed
them into the readonly Highlighter (also hiding the autosave hint).

Treat filename-absent documents as editor mode directly; filename remains the only
source for code-language inference.
…peration errors (#15273)

* ✨ feat(agent-runtime): persist ERROR_CODE_SPECS classification on operation errors

Look up the runtime error's spec in `ERROR_CODE_SPECS` at the single catch
chokepoint and merge `attribution` / `category` / `severity` / `httpStatus`
/ `retryable` / `countAsFailure` / `numericId` onto the normalized
`ChatMessageError`. The enriched object flows through to all three
downstream sinks — `agent_operations.error` JSONB, S3 trace snapshot,
and the agent-gateway WS push — without each consumer having to re-run
pattern matching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(agent-runtime): enrich inner-step error path too

Model-runtime failures caught inside `runtime.step()` resolve normally with
`newState.status = 'error'` instead of throwing, so the prior commit's outer
`executeStep` catch never sees common provider errors like
`InvalidProviderAPIKey` / `InsufficientQuota`. Those were reaching
`agent_operations.error` JSONB and the success-path trace snapshot raw —
without `attribution` / `category` / `severity` / …

Run `formatErrorForState` on `stepResult.newState.error` immediately after
`runtime.step()` returns, before the state is saved to Redis, hooks are
dispatched, or the trace is finalized. Made the helper idempotent (recognizes
already-normalized `ChatMessageError` shape) so a second pass through the
outer catch can't collapse it back to `AgentRuntimeError`. Success-path
`traceRecorder.finalize` now forwards the classification fields too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…ase 1) (#15259)

* ♻️ refactor(modal): migrate confirm modals to @lobehub/ui/base-ui

Replace all `App.useApp().modal.confirm`, `Modal.confirm` and `AntModal.confirm`
call sites with the headless `confirmModal` from `@lobehub/ui/base-ui`, dropping
antd-only props (`centered`, `type`, `width`, `okButtonProps.type='primary'`,
`okButtonProps.loading`, `classNames.root`) that the base-ui imperative API does
not accept.

- 82 files touched; `modal.confirm`/`Modal.confirm` call sites now zero
- `PageEditor/store/action.ts`: drop `modal` arg from `handleDelete`
- `ResourceManager/useUploadFolder`: replace dynamic `import('antd').Modal`
- `Eval/DatasetsTab`: migrate `modal.success` to `confirmModal`

Part of LOBE-9645 Phase 1.

* ♻️ refactor(ui): migrate select/modal call sites to @lobehub/ui/base-ui

- Convert imperative-modal factories (createXxxModal + Content split) for apikey,
  creds (Create/Edit/View), provider (CreateNewProvider), and messenger LinkModal.
- Switch Select usages to base-ui Select (Messenger AgentSelect, provider sdkType).
- Restructure CreateNewProvider form to vertical layout with manual section titles
  for tighter spacing; drop FormModal/Form group nesting.
- Standardize small ActionIcon sizing via DESKTOP_HEADER_ICON_SMALL_SIZE
  (WideScreenButton, ToggleRightPanelButton, ContextDropdown, AddNewProvider).
- Fix missing title on ResourceManager delete confirm modal so the header
  (title + close X) renders.
- Update react skill and AGENTS.md to require base-ui priority over root @lobehub/ui
  / antd; expand component table and Common Mistakes with explicit base-ui rules.

* ♻️ refactor(ui): swap antd Select to base-ui Select and migrate createStyles to createStaticStyles

* ✅ test: update test mocks for base-ui confirmModal migration

* ✅ test(e2e): switch delete confirm selector to base-ui dialog role
…mespace (#15269)

Until now, every runtime error code (InvalidProviderAPIKey, ProviderBizError,
ExceededContextWindow, …) lived under `error.response.<X>` — mixed in the
same file with HTTP statuses, Plugin*, Cloud business errors, and
GoogleAIBlockReason subkeys. The `response.` prefix is a lobehub-specific
convention that has nothing to do with the underlying ErrorCode, which
made it awkward for external consumers and noisy for maintainers.

This change carves out a dedicated `modelRuntime` i18next namespace:

- `src/locales/default/modelRuntime.ts` — 34 keys, one per
  `AgentRuntimeErrorType` (or deprecated alias `QuotaLimitReached`).
  Key = the bare ErrorCode (no `response.` prefix).
- `src/locales/default/error.ts` — runtime keys removed. The file keeps
  HTTP statuses (response.400 - response.524), Plugin*, Cloud-only
  business errors (FreePlanLimit, SubscriptionPlanLimit, etc.),
  GoogleAIBlockReason.*, and the various UI-flow strings.
- Registered `modelRuntime` in `src/locales/default/index.ts` so the
  namespace appears in the typed resources map.
- Generated `locales/en-US/modelRuntime.json` + updated
  `locales/en-US/error.json` — other languages need `pnpm i18n`.

New helper `src/utils/locale/runtimeErrorMessage.ts`:

```ts
getRuntimeErrorMessage(t, code, vars)
```

Routes via `getErrorCodeSpec(code)`: returns `t('modelRuntime:<code>')`
when the code is in `ERROR_CODE_SPECS`, otherwise falls back to
`t('response.<code>')`. Callers add `'modelRuntime'` to their
`useTranslation()` namespace list.

UI consumer migrations (5 dynamic lookup sites):

- `features/Conversation/Messages/AssistantGroup/Tool/Detail/ErrorResponse.tsx`
- `features/Conversation/Error/index.tsx`
- `routes/(main)/settings/provider/features/ProviderConfig/Checker.tsx`
  (incl. the static `t('response.ConnectionCheckFailed')` call)
- `routes/(main)/(create)/video/features/GenerationFeed/VideoErrorItem.tsx`
- `routes/(main)/(create)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx`

`Description.tsx` (HTTP status renderer) stays on `response.<X>` since
its inputs are always HTTP status numbers, never runtime ErrorCodes.

Stacks on top of #15262 (the unified errors PR introduces
`getErrorCodeSpec` / `ERROR_CODE_SPECS`); base this PR there until
#15262 merges, then it auto-rebases onto canary.

Tests: lobehub type-check clean; model-runtime 3908 pass / 1 skip / 164 files.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…#15276)

✨ feat(channel): register iMessage platform with coming-soon UI gate

Activate the server-side iMessage registration that was previously
landed but un-registered, and let coming-soon entries take precedence
over server platforms with the same id so the platform stays hidden
until the desktop bridge UI ships.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* ✨ feat(model-bank): add DeepSeek V4 Pro to SiliconCloud model list

Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>

* 💰 pricing(siliconcloud): add cache hit price for DeepSeek V4 Flash

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>
…tier digit (#15278)

The three Cloud-only `ChatErrorType` codes (`FreePlanLimit`,
`InsufficientBudgetForModel`, `LobeHubModelDeprecated`) were emitted by the
managed gateway but had no spec, so they showed up unclassified on the
operation dashboards.

Rather than add a 10th `ErrorCategory` (the single-digit category prefix
1-9 is exhausted, and a 10th would break the 4-digit numericId scheme +
its validation tests), encode the OSS-vs-Cloud distinction in the
**second digit** of `numericId`: `0` = open-source runtime, `9` = Cloud-only.
Every existing code already has tier digit 0, so this is purely additive —
the category leading-digit invariant, 4-digit range, and `E####` regex all
hold unchanged.

- `taxonomy.ts` — document the tier digit, add `CLOUD_TIER_DIGIT = 9`.
- `specs.ts` — widen the spec key/`code` type to `SpecErrorCode`
  (`ILobeAgentRuntimeErrorType | CloudErrorCode`); add the three entries
  under their semantic categories with tier-9 ids: `FreePlanLimit` E2901 &
  `InsufficientBudgetForModel` E2902 (quota), `LobeHubModelDeprecated` E4901
  (request). All `attribution: user`, `countAsFailure: false`.
- `match.test.ts` — assert every spec's tier digit is 0 or 9, and the three
  Cloud codes resolve under the cloud tier.

Locale keys (`response.<code>`) for all three already exist. The
agent-gateway mirror is updated separately.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…eries (#15279)

* ✨ feat(model-runtime): add DatabasePersistError code for failed DB queries

Drizzle stringifies a failed query/transaction as `Failed query: <sql>
params: <values>`. These are harness-side persistence failures, but they
were landing in the operation dashboards as `unknown` — and worse, the
embedded SQL/parameter text (model names, error_log rows, user messages)
contains substrings that trip unrelated provider patterns, so naive
message-matching misclassified them as CapabilityNotSupported /
InsufficientQuota / ModelNotFound.

- `agentRuntime.ts` — new `DatabasePersistError` code.
- `specs.ts` — E7004 under the 7xxx Stream/Runtime (harness) bucket,
  `attribution: harness`, `countAsFailure: true`, httpStatus 500.
- `patterns.ts` — `Failed query:` substring pattern placed **first** in the
  registry. matchErrorPattern is first-match-wins, so claiming it up front
  both classifies these correctly and stops the embedded blob from matching
  anything below.
- `match.test.ts` — assert the wrap classifies as DatabasePersistError and
  that a blob embedding `InsufficientQuota` / `context length exceeded` still
  resolves to DatabasePersistError.
- `modelRuntime.ts` — en-US `DatabasePersistError` copy (others auto-translate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(model-runtime): add StateStorePersistError; stop classifying Redis aborts as provider-network

`Command aborted due to connection close` is an ioredis error — the
Redis/Upstash agent-state store dropping a queued command, not the LLM
provider's network. It was mapped to `ProviderNetworkError`, which
misattributed our own infra failures to upstream providers.

- `agentRuntime.ts` — new `StateStorePersistError` (sibling of
  `DatabasePersistError`: DB layer vs state-store layer).
- `specs.ts` — E7005 under 7xxx Stream/Runtime (harness), countAsFailure true.
- `patterns.ts` — repoint `Command aborted due to connection close` to
  StateStorePersistError, and add the other Upstash state-store signatures
  (`max request size exceeded`, `database has been suspended`).
- `match.test.ts` + `modelRuntime.ts` — test + en-US locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(model-runtime): add ContextEnginePipelineError + harness JS-crash patterns

Classify the harness-side crashes that were landing as `unknown`:

- `ContextEnginePipelineError` (E7006, 7xxx Stream/Runtime, harness) — the
  context-engine pipeline processor crash, surfaced as "Processor [<name>]
  execution failed". The context-engine throws `PipelineError` (its
  `error.name`), so a CODE_ALIASES entry resolves `PipelineError` →
  ContextEnginePipelineError for stored / live records.
- patterns: `Processor [` → ContextEnginePipelineError, placed before the
  generic JS-crash fallbacks so a processor crash with a nested TypeError is
  attributed to the pipeline, not the bare `Cannot read properties` rule.
- patterns: bare V8 crashes (`is not a function`, `Cannot read properties of`,
  `Maximum call stack size exceeded`) → AgentRuntimeError, kept LAST so
  specific provider/harness patterns win first.
- test + en-US locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): reattribute ConversationParentMissing to user

The broken conversation chain (`parent_id` no longer exists) is usually the
user deleting the topic / parent message mid-operation — an expected race,
not a harness bug. Flip attribution harness → user, countAsFailure
true → false (so it drops out of failure metrics), severity error → warning.

numericId 7003 / category `stream` stay put (append-only); attribution and
category are orthogonal, so a stream-bucket code can be user-attributed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(model-runtime): classify "[object Object]" messages as AgentRuntimeError

A message of literally "[object Object]" means the harness stringified an
error object instead of extracting its message — a harness serialization bug.
Add it to the JS-crash fallbacks (last, lowest priority) so it resolves to
AgentRuntimeError instead of staying unknown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* ♻️ refactor(agent): run client sub-agent as a normal tool call

Make lobe-agent callSubAgent/callSubAgents execute the sub-agent in an
isolated thread via the current client runtime (executeClientAgent +
threadId + isSubAgent) and return a normal tool result, instead of the
stop:true + exec_sub_agent instruction + polling detour. UI now mirrors
the Claude Code Agent tool: a collapsed tool row that opens the sub-agent
thread in the portal. No more role='task' messages on the lobe-agent path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(agent): refine sub-agent tool UI and unify subagent thread display

- Inspector mirrors the Claude Code Agent tool: leading bot icon, "Call SubAgent" / "Call SubAgents" label, description as a chip, and a compact run-stats tail (model · tools · tokens)
- callSubAgents collapses to the first description + "等 X 个" beyond 2, with per-row stats
- rename the open-thread action to "View Detail"
- unify subagent-thread detection on ThreadType.Isolation so lobe-agent sub-agent threads indent in the sidebar and render read-only like CC subagents
- fix: refresh threads right after creating the client sub-agent thread so the "View Detail" button and sidebar entry appear immediately instead of only after a topic switch

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(agent): unify sub-agent workflow group label to "Call SubAgent"

Align the collapsed workflow group summary (workflow.toolDisplayName) with the
inspector copy so callSubAgent / callSubAgents read "Call SubAgent" / "Call
SubAgents" instead of "Dispatched a sub-agent".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…in (#15288)

* 🐛 fix(conversation-flow): guard collectAssistantChain against cyclic chains

collectAssistantChain checked `processedIds` for loop protection but never
populated it, so when a topic contains duplicated tool_call_ids (the same
tool result reachable from multiple assistant messages) the assistant→tool→
assistant walk revisited already-seen assistants and recursed without bound,
crashing the conversation view with "Maximum call stack size exceeded".
Mark each assistant visited up front.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✅ test(conversation-flow): cover collectAssistantChain cycle guard

Regression test for the duplicate-tool_call_id cycle that previously
overflowed the stack: two assistant turns declaring the same tool_call_id
make one turn's tool result resolvable from the other, so the
assistant→tool→assistant walk revisits an already-collected assistant.
Asserts the walk terminates and collects each assistant once, plus a
control case for a normal acyclic chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(conversation-flow): skip already-visited followers in collectAssistantChain

The cycle guard stopped the infinite recursion but, with a duplicated
tool_call_id, collectToolMessages can surface an earlier turn's tool result
before the current assistant's own. Its child is an already-visited assistant,
so the recursive call is a no-op — yet the unconditional return after it made
the walk stop there and silently drop the current turn's real continuation
under a later tool. Skip already-processed followers so the loop advances to
the current assistant's own tool result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…wn (#15283)

* 🐛 fix(chat-input): keep input mounted while intervention panel is shown

Conditional render swapped <DesktopChatInput> with <InterventionBar>,
unmounting the Lexical editor and wiping any unsent draft. Wrap the
input area in a display: contents | none container so the editor's
React subtree stays mounted and its in-memory document survives.

* 🐛 fix: hide expanded chat input during interventions
shell.openPath() does not perform tilde expansion, so paths like
~/git/work failed silently. Run expandTilde() (shared with the rest
of LocalFileCtr) on the incoming path before handing it to the OS.
…5289)

In the batch path (CLI / sandbox without --include-partial-messages),
the adapter extracted thinking and text from the complete assistant
block and emitted text first, reasoning second. This reversed order
caused `gatewayEventHandler` to call `startReasoningIfNeeded()` AFTER
text had already been dispatched, making the brain icon appear below
the rendered text content instead of preceding it.

Fix: swap the emission order so reasoning is always emitted before
text in both the main-agent and subagent batch paths, matching Claude's
natural output order (thinking → response) and the streaming delta path.

The desktop driver uses --include-partial-messages (partial deltas
arrive in correct order naturally), so it is unaffected.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…assify catch-all at write time (#15286)

* ✨ feat(model-runtime): split ProviderBizError into finer codes + reclassify catch-all at write time

Add UpstreamGatewayError (E8010), UpstreamMalformedResponse (E8011), and
UpstreamHttpError (E8012), migrating the matching patterns out of the
ProviderBizError catch-all. Add a refineErrorCode() step (message-pattern match
+ HTTP-status fallback) wired into formatErrorForState so generic ProviderBizError
is reclassified into the correct existing code (rate-limit / quota / network /
service-unavailable / model-not-found) instead of collapsing into one opaque
8xxx bucket. Production sampling showed ~72% of ProviderBizError actually belongs
to existing codes and only ~5% is a true residual.

* ✨ feat(model-runtime): add isFallback flag to mark catch-all error buckets

Add an `isFallback` boolean to ErrorCodeSpec / ChatMessageError, set on the
catch-all codes (ProviderBizError, UpstreamHttpError, AgentRuntimeError,
DatabasePersistError). It flows onto agent_operations.error via the write-path
enrichment so monitoring can track how much volume still lands in fallback
buckets — the signal for where finer codes are still worth carving out.

* ✅ test(model-runtime): add refineErrorCode to @lobechat/model-runtime mocks

formatErrorForState now imports refineErrorCode, so the partial module mocks in
AgentRuntimeService / RuntimeExecutors must expose it or vitest throws on access.

* ✅ test(model-runtime): bump UpstreamGatewayError numericId to 8011 after canary 8010 collision

canary claimed 8010 for ProviderContentPolicyViolation, so the Upstream* codes
shifted to 8011/8012/8013 during rebase; update the refinement test assertion.
…15291)

♻️ refactor(bot): drop iMessage desktopDeviceId + webhookSecret from user schema

These are not user-supplied: the Desktop client fills the device id from the
local gateway and generates the webhook secret on first save. Removing them
from the platform schema keeps the iMessage setup form to the fields the user
actually edits.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
… document shares migrations (#15280)

* 🔨 feat(db): batch topic usage stats, push tokens, tasks editor_data & document shares

Bundle four independent schema changes onto one migration branch:

- 0104 topics: add usage/cost aggregate columns (total_cost, token totals,
  cost/usage jsonb, model, provider) + model/provider indexes
- 0105 push_tokens: new table for Expo push notification tokens
- 0106 tasks: add editor_data jsonb column
- 0107 document_shares: new table for document share flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): combine batch schema changes into a single migration

Squash the four sequential migrations (0104-0107) into one 0104 SQL file
containing all DDL: topic usage/cost columns, push_tokens table,
tasks.editor_data column, and document_shares table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): make push_tokens unique constraint device-only

Drop the userId prefix from the push_tokens unique index — one row per
device, reassigned to the new user on switch (upsert by deviceId).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✨ feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ✨ feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(db): merge connectorTool schema into connector.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ⏪ revert(db): restore push_tokens unique constraint to (userId, deviceId)

This reverts commit addf14c (device-only unique index).

The device-only index conflicts with #15186's pushToken upsert, whose
onConflict target is (userId, deviceId). Restore the composite unique
index so the upsert lands consistently with both PRs.

Also re-point 0105 snapshot prevId to the restored 0104 id and carry the
(userId, deviceId) index forward so the migration chain stays consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(db): add devices table and consolidate batch migration into 0104

Add the `devices` identity anchor (surrogate uuid PK + unique(userId, deviceId))
as the stable, reinstall-proof base for binding agent runtime instances per
machine. Fold the prior 0104/0105 migrations and the new table into a single
idempotent 0104 migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ✅ test(db): add topic usage/cost columns to topic.create assertions

The batch added 8 nullable topic columns (totalCost/usage/model/...) but
topic.create.test.ts still asserted the pre-batch 19-field shape via toEqual.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(db): use uuid primary key for document_shares

Align document_shares.id with the other new batch tables (uuid defaultRandom);
table has no consumers yet so no compat impact. Regenerated 0104 + snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: ONLY-yours <1349021570@qq.com>
* ✨ feat(desktop): show zoom level HUD on Cmd+/- and Cmd+0

Replace Electron built-in zoomIn/zoomOut/resetZoom menu roles with custom
handlers backed by a new ZoomService, which clamps the zoom level to
[-3, +3] and broadcasts zoom:changed to the renderer. The renderer mounts
a macOS-style frosted HUD that fades in for 1.5s after each zoom change
so users can see the resulting percentage and confirm when they're back
to 100%.

* ⌨️ fix(desktop): preserve plus zoom shortcut
✨ feat(bot): add iMessage Desktop bridge with Labs gate

Desktop-side BlueBubbles bridge for the iMessage channel:

- Bridge runtime (ImessageBridgeCtr/Srv) + gateway message_api_request routing;
  chat-adapter-imessage api lists all webhooks instead of the 500-prone url
  filter (first-time save no longer fails).
- iMessage channel UI: desktopDeviceId + webhookSecret are auto-filled/generated
  (not user fields); a single "Save Configuration" persists both the cloud
  provider and the local bridge via a post-save extension point — no separate
  "Save Bridge" button.
- Gated behind the `enableImessage` Labs preference (off → "Coming Soon").
- Group local-testing bot skills into per-channel folders + add iMessage
  bridge/outbound regression scripts.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…ve) (#15299)

Server-side foundation for the device registry. Builds on the `devices` table
(already on canary) so devices persist beyond the gateway's in-memory WS
sessions and stay visible/bindable while offline.

- new DeviceModel: register upserts on (userId, deviceId) and only refreshes
  machine-reported fields + lastSeenAt, so user-owned friendlyName / defaultCwd
  / recentCwds survive re-registration
- device.* router gains register / updateDevice / removeDevice (DB row only, no
  OIDC token revocation); listDevices is rewritten as a DB ∪ online union so
  offline devices stay listed and not-yet-registered online devices surface as
  transient entries
- HeteroDeviceSwitcher adapts to the richer listDevices shape (null-safe
  platform, prefers friendlyName)

Desktop / CLI auto-registration ships in a follow-up PR that depends on this.

Part of LOBE-9572. Closes LOBE-9575.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
# 🚀 LobeHub Release (20260528)

**Release Date:** May 28, 2026  
**Since v2.2.0:** 220 merged PRs · 15 contributors

> This cycle brings heterogeneous "platform agents" you can dispatch to
local or remote devices, a rebuilt onboarding flow, document-centric
chat, and a unified model-runtime error model — with new DeepSeek V4 and
Gemini 3.5 Flash support along the way.

---

## ✨ Highlights

- **More Hetero Agents (OpenClaw / Hermes)** — Create heterogeneous
agents and dispatch them to local or remote devices through the device
gateway, with an execution-target switcher in the composer and
persistent CLI sessions. (#15065, #15179, #15022)
- **iMessage on Desktop** — New iMessage setup and bridge on desktop,
plus bot attachments across every platform. (#15228, #15227, #15029)
- **Skills in the Composer** — Drag skill chips into chat, trigger
installed skills from the slash menu mid-line, and surface project-level
skills in the homogeneous agent runtime. (#15095, #15061, #15110)
- **New Models** — DeepSeek V4 Flash/Pro and Gemini 3.5 Flash across
providers, with thinking params for structured output and chat cost
estimates. (#15031, #15001, #15051, #14876)
- **Agent Runtime Observability** — OpenTelemetry GenAI semantic
conventions plus per-call generation tracing. (#15123, #15124)

---

## 🤖 Agents & Heterogeneous Runtime

- **Platform agent creation** — OpenClaw/Hermes creation UI, device
guard, and remote dispatch backend. (#15065)
- **Execution-target switcher** — Pick local vs remote execution
directly in the composer; device-selection UX with actionable guidance.
(#15179, #15111)
- **CLI hetero dispatch** — OpenClaw/Hermes dispatch with persistent
sessions and a notify protocol. (#15022)
- **Gateway snapshot as source of truth** — Consume the gateway
`uiMessages` snapshot at step boundaries to keep chat state consistent.
(#15153, #15152)
- **Client sub-agent as a normal tool call** — Simplifies the sub-agent
execution path. (#15281)
- **Hermes agent chain** — Implements the Hermes agent chain logic.
(#15189)
- **Device registry** — TRPC endpoints to register, list, update, and
remove devices. (#15299)
- **Desktop device routing** — Route gateway agent runs through `lh
hetero exec`; restore `userId` in gateway dispatch and gate local-system
by execution target. (#15132, #15232)
- **Agent signals** — Anchor agent-signal receipts to messages and
isolate memory-agent messages into a child thread. (#14969, #14921)

---

## 🚀 Onboarding

- **Simplified first screen** — Defer topic creation to first send.
(#15090)
- **Market Agent Picker** — Added as a classic onboarding step, with
template prefetch. (#14980, #15041)
- **Welcome guidance** — Show agent welcome guidance on first run.
(#15098)
- **Mobile** — Adapt agent onboarding UI and restore Classic-step
padding on mobile. (#15019, #15032)
- **Discovery** — Streamline discovery to a single profession question.
(#14987)
- **Analytics** — Track onboarding step events and create-agent modal
source. (#15133, #15028)

---

## 📄 Documents, Pages & Knowledge

- **Thread chat in preview** — Embed thread chat in the document preview
portal. (#15216)
- **Non-markdown rendering** — Render non-markdown docs as a read-only
highlight. (#15272)
- **Multi-select** — Multi-select delete in the document tree. (#15125)
- **Page-agent streaming** — Preview `initPage` streaming arguments.
(#15039)
- **Per-agent topics** — Per-agent topic management page. (#15207)
- **Server-side category** — Derive document category server-side and
drop frontend predicates. (#15076)

---

## 🧩 Skills & Tools

- **Drag skill chips** — Drag skills into chat input and register
agent-document skills. (#15095)
- **Slash menu** — Installed skills appear in the slash menu with a
mid-line trigger. (#15061)
- **Project skills** — Recognize project-level skills in the homogeneous
agent runtime and surface them regardless of active device. (#15110,
#15177)
- **VFS archiving** — Archive oversized tool results to VFS instead of
truncating. (#15074)
- **@localfile mentions** — Drag folders into chat input as `@localFile`
mentions on desktop. (#15071)

---

## 🧠 Model Runtime & Providers

- **Error spec registry** — Unify error codes into a spec + pattern
registry, split `ProviderBizError` into finer codes, classify Cloud-only
codes via a tier digit, and add `DatabasePersistError`. (#15262, #15286,
#15278, #15279)
- **New models** — DeepSeek V4 Flash/Pro (opencode-go) and Gemini 3.5
Flash; DeepSeek V4 Pro on SiliconCloud. (#15031, #15001, #15017, #15267)
- **Structured output** — Thinking params for structured output, Bedrock
structured generation, and DeepSeek `generateObject` tool choice.
(#15051, #15174, #15054)
- **Cost** — Chat cost estimate support; preserve usage cost in custom
streams. (#14876, #15218)

---

## 💬 Chat & User Experience

- **Follow-up chips** — Extend follow-up chip suggestions to general
chat with scene-specific model config. (#15101, #14797)
- **Input drafts** — Persist unsent input drafts across tab switches and
prevent repeated draft restore. (#14992, #15024)
- **Command menu** — Order topic/message search by recency and promote
inline type filters. (#15094, #14986)
- **Zoom HUD** — Show a zoom-level HUD on Cmd +/− and Cmd 0. (#15294)
- **Copy** — Unescape markdown escapes when copying user messages.
(#15253)

---

## 🖥️ Desktop

- **App Nap fix** — Prevent App Nap from dropping the gateway WebSocket
during display sleep. (#14994)
- **File preview** — Preview `.cjs`/`.mjs`/no-extension files instead of
binary fallback and expand `~` when opening local files. (#15168,
#15284)
- **Cross-platform settings** — Open settings via main-window navigation
on Windows/Linux and restore the route after an update restart. (#15036,
#14922)
- **Token refresh** — Prevent frequent logout from token-refresh
retries. (#14928)

---

## 📊 Observability

- **OTel GenAI** — Instrument Agent Runtime with OpenTelemetry GenAI
semantic conventions. (#15123)
- **Generation tracing** — Per-call `llm_generation_tracing` with a
pre-allocated tracingId and recordFeedback router. (#15124, #15146)
- **Error classification** — Persist `ERROR_CODE_SPECS` classification
on operation errors. (#15273)

---

## 🗃️ Database Migrations

- **Batch migrations** — Topic usage stats, push tokens,
`tasks.editor_data`, and document shares. (#15280)
- **Tracing & eval tables** — Add `llm_generation_tracing` and agent
eval experiment tables. (#15126)

> Self-hosted operators should run the database migration (`pnpm
db:migrate`, or restart with auto-migrate enabled) after upgrading. The
changes are additive and backwards-compatible.

---

## 🔒 Security & Reliability

- **Security:** Remove the `getPlaintextCred` tool to prevent plaintext
credential exposure. (#14998)
- **Security:** Prompt account selection for Google OAuth and add
`prompt=consent` to the OIDC authorization URL to fix missing refresh
tokens. (#15234, #15010)
- **Reliability:** Preserve streamed content across a mid-stream cancel.
(#15173)
- **Reliability:** Bound the Redis command timeout and configure the
Anthropic client timeout. (#15091, #15042)
- **Reliability:** Prevent infinite recursion in the assistant chain.
(#15288)

---

## 👥 Contributors

Huge thanks to **15 contributors** who shipped **220 merged PRs** this
cycle.

@AnotiaWang · @sxjeru · @algojogacor · @hardy-one · @arvinxx · @Innei ·
@tjx666 · @lijian · @AmAzing129 · @rdmclin2 · @neko · @cy948 ·
@CanisMinor · @sudongyuer · @rivertwilight

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**: v2.2.0...release/weekly-20260528
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.